package com.spring.framework;

import java.beans.Introspector;
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

/**
 * @author yangjian
 */
public class AnnotationConfigApplicationContext {

	private HashMap<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
	private HashMap<String, Object> singletonObjectsMap = new HashMap<>();

	private List<BeanPostProcessor> beanPostProcessorList = new ArrayList<>();

	public AnnotationConfigApplicationContext(Class<?> configClass)
	{
		// scan beans
		scanComponents(configClass);

		// create beans
		beanDefinitionMap.forEach((String beanName, BeanDefinition beanDefinition) -> {
			if (beanDefinition.getScope().equals(Scope.SINGLETON)) {
				Object bean = createBean(beanName, beanDefinition);
				singletonObjectsMap.put(beanName, bean);
			}
		});

	}

	public void scanComponents(Class<?> configClass)
	{
		if (configClass.isAnnotationPresent(ComponentScan.class)) {
			ComponentScan componentScanAnnotation = configClass.getAnnotation(ComponentScan.class);
			String[] basePaths = componentScanAnnotation.values();
			for (String path : basePaths) {
				path = path.replace(".", File.separator);
				ClassLoader classLoader = AnnotationConfigApplicationContext.class.getClassLoader();
				URL resource = classLoader.getResource(path);

				// load all class file
				assert resource != null;
				File file = new File(resource.getFile());
				if (file.isDirectory()) {
					scanComponentsPath(file, classLoader);
				}
			}
		}
	}

	public void scanComponentsPath(File dir, ClassLoader classLoader)
	{
		for (File file : Objects.requireNonNull(dir.listFiles())) {
			if (file.isDirectory()) {
				scanComponentsPath(file, classLoader);
				continue;
			}

			String absolutePath = file.getAbsolutePath();
			String classPath = absolutePath.substring(absolutePath.indexOf("com"), absolutePath.indexOf(".class"));
			classPath = classPath.replace(File.separator, ".");
			try {
				Class<?> clazz = classLoader.loadClass(classPath);
				// create bean processor
				if (clazz.isAnnotationPresent(BeanProcessor.class) || BeanPostProcessor.class.isAssignableFrom(clazz)) {
					beanPostProcessorList.add((BeanPostProcessor) clazz.getConstructor().newInstance());
				}

				// check if the class has a Component annotation
				if (clazz.isAnnotationPresent(Component.class)) {
					Component componentAnnotation = clazz.getAnnotation(Component.class);
					String beanName = componentAnnotation.value();
					if ("".equals(beanName)) {
						// get class simple name as the bean name
						beanName = Introspector.decapitalize(clazz.getSimpleName());
					}

					BeanDefinition beanDefinition = new BeanDefinition();
					beanDefinition.setType(clazz);

					if (clazz.isAnnotationPresent(Scope.class)) {
						Scope scopeAnnotation = clazz.getAnnotation(Scope.class);
						String scope = scopeAnnotation.value();
						beanDefinition.setScope(scope);
					} else {
						beanDefinition.setScope(Scope.SINGLETON);
					}

					// save BeanDefinitionMap
					beanDefinitionMap.put(beanName, beanDefinition);
				}
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	public Object getBean(String beanName)
	{
		if (!beanDefinitionMap.containsKey(beanName)) {
			throw new RuntimeException("Bean not found for beanName " + beanName);
		}

		BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
		if (beanDefinition.getScope().equals(Scope.SINGLETON)) {
			// bean not created
			if (null != singletonObjectsMap.get(beanName)) {
				return singletonObjectsMap.get(beanName);
			}
		}
		// create a new bean
		Object bean = createBean(beanName, beanDefinition);
		singletonObjectsMap.put(beanName, bean);
		return bean;
	}

	public Object createBean(String beanName, BeanDefinition beanDefinition)
	{
		Class<?> clazz = beanDefinition.getType();
		Object instance;
		try {
			instance = clazz.getConstructor().newInstance();
			// TODO: constructor with parameters?

			// inject properties
			for (Field field : clazz.getDeclaredFields()) {
				if (!field.isAnnotationPresent(AutoWired.class)) {
					continue;
				}
				injectFiled(field, instance);
			}

			for (BeanPostProcessor processor : beanPostProcessorList) {
				instance = processor.postProcessBeforeInitialization(instance, beanName);
			}

			// initialize bean if implemented InitializingBean interface
			if (instance instanceof InitializingBean) {
				((InitializingBean) instance).afterPropertiesSet();
			}

			for (BeanPostProcessor processor : beanPostProcessorList) {
				instance = processor.postProcessAfterInitialization(instance, beanName);
			}

		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		return instance;
	}

	public void injectFiled(Field field, Object instance) throws IllegalAccessException
	{
		String beanName = field.getAnnotation(AutoWired.class).value();
		if ("".equals(beanName)) {
			beanName = field.getName();
		}
		field.setAccessible(true);
		field.set(instance, getBean(beanName));
		// TODO: inject bean by type
	}
}
